namespace MetaProperties.SynchronizationContexts
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Security.Permissions;
    using System.Threading;

    [SecurityPermission(SecurityAction.Demand, ControlThread = true)]
    public class ThreadPoolSynchronizationContext : SynchronizationContext, IDisposable
    {
        private readonly WorkerThread[] workerThreads;

        private readonly Queue<WorkItem> workItemQueue;

        public ThreadPoolSynchronizationContext(uint poolSize) 
            : this(poolSize, "TPSC")
        {
        }

        public ThreadPoolSynchronizationContext(uint poolSize, string poolName)
        {
            if (poolSize == 0)
            {
                throw new InvalidOperationException("Pool size cannot be zero");
            }

            this.ItemAdded = new Semaphore(0, Int32.MaxValue);
            this.workItemQueue = new Queue<WorkItem>();

            this.workerThreads = new WorkerThread[poolSize];
            for (int index = 0; index < poolSize; index++)
            {
                this.workerThreads[index] = new WorkerThread(poolName + " " + (index + 1), this);
            }
        }

        protected Semaphore ItemAdded
        {
            get;
            set;
        }

        protected virtual bool QueueEmpty
        {
            get
            {
                lock (this.workItemQueue)
                {
                    if (this.workItemQueue.Count > 0)
                    {
                        return false;
                    }

                    return true;
                }
            }
        }

        public void Abort()
        {
            try
            {
                this.ItemAdded.Release(Int32.MaxValue);
            }
            catch (SemaphoreFullException)
            {
                // We can ignore this as by the above line it seems to be expected.
            }

            foreach (WorkerThread thread in this.workerThreads)
            {
                thread.thread.Abort();
            }

            this.ItemAdded.Close();
        }

        public void Close()
        {
            if (this.ItemAdded.SafeWaitHandle.IsClosed)
            {
                return;
            }

            try
            {
                this.ItemAdded.Release(Int32.MaxValue);
            }
            catch (SemaphoreFullException)
            {
                // We can ignore this as by the above line it seems to be expected.
            }

            foreach (WorkerThread thread in this.workerThreads)
            {
                thread.Kill();
            }

            this.ItemAdded.Close();
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }

        public void Dispose()
        {
            this.Close();
        }

        public override void Post(SendOrPostCallback method, object state)
        {
            WorkItem workItem = new WorkItem(method, state);
            this.QueueWorkItem(workItem);
        }

        public override void Send(SendOrPostCallback method, object state)
        {
            // If we're already in the correct context, 
            // we must invoke now to avoid deadlock.
            if (SynchronizationContext.Current == this)
            {
                method(state);
                return;
            }

            WorkItem workItem = new WorkItem(method, state);
            this.QueueWorkItem(workItem);
            workItem.WaitHandle.WaitOne();
        }

        internal virtual WorkItem GetNext()
        {
            this.ItemAdded.WaitOne();
            lock (this.workItemQueue)
            {
                if (this.workItemQueue.Count == 0)
                {
                    return null;
                }

                return this.workItemQueue.Dequeue();
            }
        }

        internal virtual void QueueWorkItem(WorkItem workItem)
        {
            lock (this.workItemQueue)
            {
                this.workItemQueue.Enqueue(workItem);
                this.ItemAdded.Release();
            }
        }

        private class WorkerThread
        {
            public readonly Thread thread;

            private readonly ThreadPoolSynchronizationContext context;

            private bool endLoop;

            internal WorkerThread(string name, ThreadPoolSynchronizationContext context)
            {
                this.context = context;

                this.endLoop = false;
                this.thread = null;

                this.thread = new Thread(this.Run);
                this.thread.IsBackground = true;
                this.thread.Name = name;
                this.thread.Start();
            }

            public int ManagedThreadId
            {
                get { return this.thread.ManagedThreadId; }
            }

            private bool EndLoop
            {
                set
                {
                    lock (this)
                    {
                        this.endLoop = value;
                    }
                }

                get
                {
                    lock (this)
                    {
                        return this.endLoop;
                    }
                }
            }

            public void Kill()
            {
                // Kill is called on client thread - must use cached thread object.
                Debug.Assert(this.thread != null);
                if (this.thread.IsAlive == false)
                {
                    return;
                }

                this.EndLoop = true;

                // Wait for thread to die
                this.thread.Join();
            }

            private void Run()
            {
                Debug.Assert(SynchronizationContext.Current == null);
                SynchronizationContext.SetSynchronizationContext(this.context);

                while (this.EndLoop == false)
                {
                    WorkItem workItem = this.context.GetNext();
                    if (workItem != null)
                    {
                        workItem.Execute();
                    }
                }
            }

            private void Start()
            {
                Debug.Assert(this.thread != null);
                Debug.Assert(this.thread.IsAlive == false);
                this.thread.Start();
            }
        }
    }
}